Von einfachen URL-Patterns zu automatischem Routing mit Routers
Request → URL Pattern → View → Response
# filepath: movieapi/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('movies/', include('movies.urls')), # ← App URLs einbinden
]
# filepath: movies/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.movie_list, name='movie-list'), # /movies/
path('<int:pk>/', views.movie_detail, name='movie-detail'), # /movies/1/
]
# filepath: movies/urls.py
from django.urls import path
from .views import MovieListAPIView, MovieDetailAPIView
urlpatterns = [
path('movies/', MovieListAPIView.as_view()),
path('movies/<int:pk>/', MovieDetailAPIView.as_view()),
]
✅ Maximale Kontrolle
❌ Viel Code
# filepath: movies/urls.py
from django.urls import path
from .views import MovieListCreateView, MovieDetailView
urlpatterns = [
path('movies/', MovieListCreateView.as_view()),
path('movies/<int:pk>/', MovieDetailView.as_view()),
]
✅ Weniger Code
❌ Immer noch manuell
# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter
from .views import MovieViewSet
router = DefaultRouter()
router.register(r'movies', MovieViewSet)
urlpatterns = router.urls
✅ Automatische URLs
✅ RESTful Standard
⭐ Empfohlen!
from django.urls import path
urlpatterns = [
# Integer Parameter
path('movies/<int:pk>/', views.movie_detail),
# /movies/1/
# String Parameter
path('movies/<str:slug>/', views.movie_by_slug),
# /movies/the-matrix/
# UUID Parameter
path('movies/<uuid:id>/', views.movie_by_uuid),
# /movies/550e8400-e29b-41d4-a716-446655440000/
# Path Parameter (mehrere Segmente)
path('files/<path:filepath>/', views.file_detail),
# /files/docs/2023/report.pdf
]
✅ Vorteile: Einfach, lesbar, typsicher
from django.urls import re_path
urlpatterns = [
# Jahr (4 Ziffern)
re_path(r'^movies/(?P<year>[0-9]{4})/$', views.movies_by_year),
# /movies/2023/
# Slug (Buchstaben, Zahlen, Bindestriche)
re_path(r'^movies/(?P<slug>[-\w]+)/$', views.movie_by_slug),
# /movies/the-matrix-1999/
# Custom Pattern
re_path(r'^api/v(?P<version>[0-9]+)/movies/$', views.movie_list),
# /api/v1/movies/, /api/v2/movies/
]
✅ Vorteile: Maximale Flexibilität
❌ Nachteile: Komplexer, fehleranfällig
# filepath: movies/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Movie
from .serializers import MovieSerializer
class MovieListAPIView(APIView):
"""GET: Liste, POST: Erstellen"""
def get(self, request):
movies = Movie.objects.all()
serializer = MovieSerializer(movies, many=True)
return Response(serializer.data)
def post(self, request):
serializer = MovieSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class MovieDetailAPIView(APIView):
"""GET: Detail, PUT: Update, DELETE: Löschen"""
def get(self, request, pk):
movie = get_object_or_404(Movie, pk=pk)
serializer = MovieSerializer(movie)
return Response(serializer.data)
def put(self, request, pk):
movie = get_object_or_404(Movie, pk=pk)
serializer = MovieSerializer(movie, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk):
movie = get_object_or_404(Movie, pk=pk)
movie.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
# filepath: movies/urls.py
from django.urls import path
from .views import MovieListAPIView, MovieDetailAPIView
urlpatterns = [
path('movies/', MovieListAPIView.as_view(), name='movie-list'),
path('movies/<int:pk>/', MovieDetailAPIView.as_view(), name='movie-detail'),
]
# filepath: movieapi/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('movies.urls')), # ← Präfix 'api/'
]
GET /api/movies/ → Liste aller Filme
POST /api/movies/ → Film erstellen
GET /api/movies/1/ → Film #1 Details
PUT /api/movies/1/ → Film #1 aktualisieren
DELETE /api/movies/1/ → Film #1 löschen
# filepath: movies/views.py
from rest_framework import generics
from .models import Movie
from .serializers import MovieSerializer
class MovieListCreateView(generics.ListCreateAPIView):
"""GET: Liste, POST: Erstellen"""
queryset = Movie.objects.all()
serializer_class = MovieSerializer
class MovieDetailView(generics.RetrieveUpdateDestroyAPIView):
"""GET: Detail, PUT/PATCH: Update, DELETE: Löschen"""
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# filepath: movies/urls.py
from django.urls import path
from .views import MovieListCreateView, MovieDetailView
urlpatterns = [
path('movies/', MovieListCreateView.as_view(), name='movie-list'),
path('movies/<int:pk>/', MovieDetailView.as_view(), name='movie-detail'),
]
2 Views × ~30 Zeilen = 60 Zeilen
2 URL-Patterns
2 Views × 3 Zeilen = 6 Zeilen
2 URL-Patterns (gleich)
GET /api/movies/ → Liste aller Filme
POST /api/movies/ → Film erstellen
GET /api/movies/1/ → Film #1 Details
PUT /api/movies/1/ → Film #1 komplett aktualisieren
PATCH /api/movies/1/ → Film #1 teilweise aktualisieren
DELETE /api/movies/1/ → Film #1 löschen
URLs werden automatisch mit Routers generiert
# filepath: movies/views.py
from rest_framework import viewsets
from .models import Movie
from .serializers import MovieSerializer
class MovieViewSet(viewsets.ModelViewSet):
"""
ViewSet für Movie CRUD-Operationen.
Automatisch bereitgestellt:
- list(): GET /movies/
- create(): POST /movies/
- retrieve(): GET /movies/{pk}/
- update(): PUT /movies/{pk}/
- partial_update(): PATCH /movies/{pk}/
- destroy(): DELETE /movies/{pk}/
"""
queryset = Movie.objects.all()
serializer_class = MovieSerializer
Das war's! Nur 3 Zeilen Code! 🚀
→ Mit einem Router!
# filepath: movies/urls.py
from rest_framework.routers import SimpleRouter
from .views import MovieViewSet
# Router erstellen
router = SimpleRouter()
# ViewSet registrieren
router.register(r'movies', MovieViewSet)
# URLs exportieren
urlpatterns = router.urls
# filepath: movieapi/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('movies.urls')),
]
GET /api/movies/ → movie-list
POST /api/movies/ → movie-list
GET /api/movies/{pk}/ → movie-detail
PUT /api/movies/{pk}/ → movie-detail
PATCH /api/movies/{pk}/ → movie-detail
DELETE /api/movies/{pk}/ → movie-detail
# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter
from .views import MovieViewSet, ArtistViewSet
# Router erstellen
router = DefaultRouter()
# ViewSets registrieren
router.register(r'movies', MovieViewSet, basename='movie')
router.register(r'artists', ArtistViewSet, basename='artist')
# URLs exportieren
urlpatterns = router.urls
# Root API (Übersicht)
GET /api/ → api-root
# Movies
GET /api/movies/ → movie-list
POST /api/movies/ → movie-list
GET /api/movies/{pk}/ → movie-detail
PUT /api/movies/{pk}/ → movie-detail
PATCH /api/movies/{pk}/ → movie-detail
DELETE /api/movies/{pk}/ → movie-detail
# Artists
GET /api/artists/ → artist-list
POST /api/artists/ → artist-list
GET /api/artists/{pk}/ → artist-detail
PUT /api/artists/{pk}/ → artist-detail
PATCH /api/artists/{pk}/ → artist-detail
DELETE /api/artists/{pk}/ → artist-detail
GET /api/
{
"movies": "http://localhost:8000/api/movies/",
"artists": "http://localhost:8000/api/artists/"
}
❌ Keine Root View
✅ Minimalistisch
✅ Root API View
✅ Besser für Browsable API
⭐ Empfohlen!
# filepath: movies/urls.py
router = DefaultRouter()
router.register(r'movies', MovieViewSet)
# ↑ basename wird automatisch aus Model generiert: 'movie'
URL-Namen: movie-list, movie-detail
# filepath: movies/views.py
class MovieViewSet(viewsets.ModelViewSet):
serializer_class = MovieSerializer
def get_queryset(self):
"""Dynamisches QuerySet"""
user = self.request.user
if user.is_staff:
return Movie.objects.all()
return Movie.objects.filter(created_by=user)
# Kein queryset-Attribut!
# filepath: movies/urls.py
router = DefaultRouter()
router.register(r'movies', MovieViewSet, basename='movie')
# ^^^^^^^^^^^^^^^^
# basename MUSS angegeben werden!
router = DefaultRouter()
router.register(r'my-movies', MovieViewSet, basename='my-movie')
# URL-Namen: 'my-movie-list', 'my-movie-detail'
reverse('movie-list'){% url 'movie-detail' pk=1 %}HyperlinkedModelSerializerRouter generiert automatisch URLs für @action
# filepath: movies/views.py
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from django.db.models import Avg
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
@action(detail=False, methods=['get'])
def top_rated(self, request):
"""
Top 10 Filme nach Rating
URL: GET /api/movies/top_rated/
"""
top_movies = Movie.objects.order_by('-rating')[:10]
serializer = self.get_serializer(top_movies, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def recent(self, request):
"""
Filme der letzten 5 Jahre
URL: GET /api/movies/recent/
"""
current_year = datetime.now().year
recent_movies = Movie.objects.filter(year__gte=current_year - 5)
serializer = self.get_serializer(recent_movies, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def rate(self, request, pk=None):
"""
Film bewerten
URL: POST /api/movies/{pk}/rate/
"""
movie = self.get_object()
rating = request.data.get('rating')
# Validierung
if not rating or not (0 <= float(rating) <= 10):
return Response(
{'error': 'Rating must be between 0 and 10'},
status=400
)
movie.rating = rating
movie.save()
serializer = self.get_serializer(movie)
return Response(serializer.data)
@action(detail=True, methods=['get'])
def cast(self, request, pk=None):
"""
Besetzung eines Films
URL: GET /api/movies/{pk}/cast/
"""
movie = self.get_object()
castings = movie.castings.all()
serializer = MovieCastingSerializer(castings, many=True)
return Response(serializer.data)
# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter
from .views import MovieViewSet
router = DefaultRouter()
router.register(r'movies', MovieViewSet)
urlpatterns = router.urls
# Standard CRUD
GET /api/movies/ → movie-list
POST /api/movies/ → movie-list
GET /api/movies/{pk}/ → movie-detail
PUT /api/movies/{pk}/ → movie-detail
PATCH /api/movies/{pk}/ → movie-detail
DELETE /api/movies/{pk}/ → movie-detail
# Custom Actions (Collection)
GET /api/movies/top_rated/ → movie-top-rated
GET /api/movies/recent/ → movie-recent
# Custom Actions (Member)
POST /api/movies/{pk}/rate/ → movie-rate
GET /api/movies/{pk}/cast/ → movie-cast
Collection-Action
Wirkt auf Liste
/movies/action_name/
Beispiel: top_rated, recent, stats
Member-Action
Wirkt auf einzelnes Objekt
/movies/{pk}/action_name/
Beispiel: rate, cast, archive
from rest_framework.decorators import action
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
@action(
detail=False, # Collection (False) oder Member (True)?
methods=['get', 'post'], # Erlaubte HTTP-Methoden
url_path='top-rated', # Custom URL-Pfad (Standard: Funktionsname)
url_name='top-rated-movies', # Custom URL-Name (für reverse)
permission_classes=[IsAuthenticated], # Custom Permissions
serializer_class=TopMovieSerializer, # Custom Serializer
)
def top_rated(self, request):
"""Top-bewertete Filme"""
# ...
pass
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# 1. Standard (GET only)
@action(detail=False)
def recent(self, request):
"""URL: /movies/recent/"""
pass
# 2. Custom URL-Path
@action(detail=False, url_path='top-10')
def top_rated(self, request):
"""URL: /movies/top-10/"""
pass
# 3. Multiple HTTP Methods
@action(detail=True, methods=['get', 'post', 'delete'])
def bookmark(self, request, pk=None):
"""
URL: /movies/{pk}/bookmark/
GET: Check if bookmarked
POST: Add bookmark
DELETE: Remove bookmark
"""
if request.method == 'GET':
# Check bookmark
pass
elif request.method == 'POST':
# Add bookmark
pass
elif request.method == 'DELETE':
# Remove bookmark
pass
# 4. Custom Serializer
@action(
detail=False,
serializer_class=MovieStatisticsSerializer
)
def statistics(self, request):
"""URL: /movies/statistics/"""
stats = {
'total': Movie.objects.count(),
'avg_rating': Movie.objects.aggregate(Avg('rating'))['rating__avg'],
}
serializer = self.get_serializer(stats)
return Response(serializer.data)
# 5. Custom Permissions
@action(
detail=True,
methods=['post'],
permission_classes=[IsAdminUser]
)
def verify(self, request, pk=None):
"""
URL: /movies/{pk}/verify/
Nur für Admins!
"""
movie = self.get_object()
movie.is_verified = True
movie.save()
return Response({'status': 'verified'})
url_path für kebab-case URLspermission_classes wenn nötig# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter
from .views import MovieViewSet, ArtistViewSet
router = DefaultRouter()
router.register(r'movies', MovieViewSet)
router.register(r'artists', ArtistViewSet)
# Kein urlpatterns! Nur router exportieren
# filepath: reviews/urls.py
from rest_framework.routers import DefaultRouter
from .views import ReviewViewSet, CommentViewSet
router = DefaultRouter()
router.register(r'reviews', ReviewViewSet)
router.register(r'comments', CommentViewSet)
# filepath: movieapi/urls.py
from django.contrib import admin
from django.urls import path, include
# Routers importieren
from movies.urls import router as movies_router
from reviews.urls import router as reviews_router
urlpatterns = [
path('admin/', admin.site.urls),
# Router URLs einbinden
path('api/', include(movies_router.urls)),
path('api/', include(reviews_router.urls)),
# Oder mit Präfix:
# path('api/movies/', include(movies_router.urls)),
# path('api/reviews/', include(reviews_router.urls)),
]
GET /api/ → Root API
GET /api/movies/ → Movies
GET /api/artists/ → Artists
GET /api/reviews/ → Reviews
GET /api/comments/ → Comments
/movies/{id}/castings/ statt /castings/?movie={id}
# Standard (Flach):
GET /api/movies/1/
GET /api/castings/?movie=1 # ← Nicht ideal
# Gewünscht (Nested):
GET /api/movies/1/
GET /api/movies/1/castings/ # ← Besser!
# Installation:
pip install drf-nested-routers
# filepath: movies/views.py
from rest_framework import viewsets
from .models import Movie, MovieCasting
from .serializers import MovieSerializer, MovieCastingSerializer
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
class MovieCastingViewSet(viewsets.ModelViewSet):
serializer_class = MovieCastingSerializer
def get_queryset(self):
"""Nur Castings des Movies"""
movie_pk = self.kwargs.get('movie_pk')
return MovieCasting.objects.filter(movie_id=movie_pk)
def perform_create(self, serializer):
"""Movie automatisch setzen"""
movie_pk = self.kwargs.get('movie_pk')
serializer.save(movie_id=movie_pk)
# filepath: movies/urls.py
from rest_framework_nested import routers
from .views import MovieViewSet, MovieCastingViewSet
# Parent Router
router = routers.DefaultRouter()
router.register(r'movies', MovieViewSet, basename='movie')
# Nested Router
movies_router = routers.NestedDefaultRouter(
router, # Parent Router
r'movies', # Parent Prefix
lookup='movie' # Lookup-Name für {movie_pk}
)
movies_router.register(
r'castings', # Nested Prefix
MovieCastingViewSet,
basename='movie-castings'
)
# URLs kombinieren
urlpatterns = router.urls + movies_router.urls
# Movies (Standard)
GET /api/movies/
POST /api/movies/
GET /api/movies/{id}/
PUT /api/movies/{id}/
DELETE /api/movies/{id}/
# Castings (Nested)
GET /api/movies/{movie_pk}/castings/
POST /api/movies/{movie_pk}/castings/
GET /api/movies/{movie_pk}/castings/{id}/
PUT /api/movies/{movie_pk}/castings/{id}/
DELETE /api/movies/{movie_pk}/castings/{id}/
/api/v1/movies/ vs /api/v2/movies/
# filepath: movieapi/settings.py
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
'DEFAULT_VERSION': 'v1',
'ALLOWED_VERSIONS': ['v1', 'v2'],
'VERSION_PARAM': 'version',
}
# filepath: movieapi/urls.py
from django.contrib import admin
from django.urls import path, include
from movies.urls import router as movies_router_v1
from movies_v2.urls import router as movies_router_v2
urlpatterns = [
path('admin/', admin.site.urls),
# API v1
path('api/v1/', include(movies_router_v1.urls)),
# API v2
path('api/v2/', include(movies_router_v2.urls)),
]
# filepath: movies/views.py
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
def get_serializer_class(self):
"""Serializer basierend auf Version"""
if self.request.version == 'v1':
return MovieSerializerV1
elif self.request.version == 'v2':
return MovieSerializerV2
return MovieSerializer
GET /api/v1/movies/ → Version 1
GET /api/v2/movies/ → Version 2
# settings.py:
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.QueryParameterVersioning',
}
# URLs:
GET /api/movies/?version=v1
GET /api/movies/?version=v2
# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter
from django.urls import path
from .views import MovieViewSet, MovieSearchAPIView, MovieStatsAPIView
# Router für ViewSet
router = DefaultRouter()
router.register(r'movies', MovieViewSet)
# Router URLs + Custom URLs kombinieren
urlpatterns = [
# Custom URLs (nicht im ViewSet)
path('movies/search/', MovieSearchAPIView.as_view(), name='movie-search'),
path('movies/stats/', MovieStatsAPIView.as_view(), name='movie-stats'),
] + router.urls
# WICHTIG: Custom URLs VOR router.urls!
# Sonst matched /movies/search/ als /movies/{pk}/
# Custom URLs (manuell)
GET /api/movies/search/?q=matrix
GET /api/movies/stats/
# Router URLs (automatisch)
GET /api/movies/
GET /api/movies/{pk}/
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
@action(detail=False, methods=['get'])
def search(self, request):
"""URL: /movies/search/?q=matrix"""
query = request.query_params.get('q')
movies = Movie.objects.filter(title__icontains=query)
serializer = self.get_serializer(movies, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def stats(self, request):
"""URL: /movies/stats/"""
stats = {
'total': Movie.objects.count(),
'avg_rating': Movie.objects.aggregate(Avg('rating'))['rating__avg']
}
return Response(stats)
# URLs:
router = DefaultRouter()
router.register(r'movies', MovieViewSet)
urlpatterns = router.urls # Fertig!
✅ Separate Views
✅ Mehr Kontrolle
❌ Mehr Code
✅ Alles in einem ViewSet
✅ Weniger Code
✅ Automatische URLs
⭐ Empfohlen!
GET /api/movies/ ✅
GET /api/movies ❌ (Redirect zu /api/movies/)
# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter
# Trailing Slashes ausschalten
router = DefaultRouter(trailing_slash=False)
router.register(r'movies', MovieViewSet)
urlpatterns = router.urls
# Jetzt:
GET /api/movies ✅
GET /api/movies/ ❌
# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter
# Beide Varianten erlauben
router = DefaultRouter()
router.register(r'movies/?', MovieViewSet) # ← /? macht Slash optional
# Jetzt:
GET /api/movies ✅
GET /api/movies/ ✅
Sicherstellen dass Routing funktioniert
# filepath: movies/tests/test_urls.py
from django.test import TestCase
from django.urls import reverse, resolve
from rest_framework.test import APITestCase
from movies.views import MovieViewSet
class MovieURLTest(TestCase):
"""Tests für Movie URLs"""
def test_movie_list_url(self):
"""Test: movie-list URL"""
url = reverse('movie-list')
self.assertEqual(url, '/api/movies/')
def test_movie_detail_url(self):
"""Test: movie-detail URL"""
url = reverse('movie-detail', kwargs={'pk': 1})
self.assertEqual(url, '/api/movies/1/')
def test_movie_top_rated_url(self):
"""Test: Custom Action URL"""
url = reverse('movie-top-rated')
self.assertEqual(url, '/api/movies/top_rated/')
def test_movie_rate_url(self):
"""Test: Member Action URL"""
url = reverse('movie-rate', kwargs={'pk': 1})
self.assertEqual(url, '/api/movies/1/rate/')
def test_url_resolves_to_view(self):
"""Test: URL löst zu korrektem ViewSet auf"""
resolver = resolve('/api/movies/')
self.assertEqual(resolver.func.cls, MovieViewSet)
class MovieAPITest(APITestCase):
"""Tests für Movie API Endpoints"""
def test_movie_list_endpoint(self):
"""Test: GET /api/movies/"""
response = self.client.get('/api/movies/')
self.assertEqual(response.status_code, 200)
def test_movie_detail_endpoint(self):
"""Test: GET /api/movies/1/"""
# Movie erstellen
movie = Movie.objects.create(
title="Matrix",
year=1999,
genre="Sci-Fi"
)
response = self.client.get(f'/api/movies/{movie.pk}/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['title'], "Matrix")
def test_movie_create_endpoint(self):
"""Test: POST /api/movies/"""
data = {
'title': 'Inception',
'year': 2010,
'genre': 'Sci-Fi',
'rating': 8.8
}
response = self.client.post('/api/movies/', data, format='json')
self.assertEqual(response.status_code, 201)
self.assertEqual(Movie.objects.count(), 1)
def test_custom_action_top_rated(self):
"""Test: GET /api/movies/top_rated/"""
# Test-Daten erstellen
Movie.objects.create(title="Movie 1", year=2020, rating=9.0)
Movie.objects.create(title="Movie 2", year=2021, rating=8.5)
response = self.client.get('/api/movies/top_rated/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 2)
def test_url_not_found(self):
"""Test: Nicht existierende URL"""
response = self.client.get('/api/movies/99999/')
self.assertEqual(response.status_code, 404)
# ✅ EMPFOHLEN:
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'movies', MovieViewSet)
urlpatterns = router.urls
# ✅ GUT (RESTful):
/api/movies/
/api/movies/1/
/api/movies/1/castings/
/api/artists/
# ❌ SCHLECHT (inkonsistent):
/api/getMovies/
/api/movie/1/
/api/getCastings?movieId=1
/api/artist-list/
# ✅ Automatisch (wenn queryset vorhanden):
router.register(r'movies', MovieViewSet)
# ✅ Manuell (wenn kein queryset):
router.register(r'movies', MovieViewSet, basename='movie')
# ❌ FEHLER:
router.register(r'movies', MovieViewSet)
# → Error: queryset attribute required!
# ✅ BESSER (als @action):
class MovieViewSet(viewsets.ModelViewSet):
@action(detail=False)
def top_rated(self, request):
pass
# ❌ SCHLECHTER (Custom URL):
urlpatterns = [
path('movies/top-rated/', TopRatedAPIView.as_view()),
] + router.urls
# ✅ GUT (reverse):
from django.urls import reverse
url = reverse('movie-detail', kwargs={'pk': 1})
# ❌ SCHLECHT (hardcoded):
url = '/api/movies/1/'
# ✅ Bei großen Änderungen:
/api/v1/movies/ # Old API
/api/v2/movies/ # New API
# ❌ Nicht für jede kleine Änderung!
# ✅ Nested sinnvoll:
/api/movies/1/castings/
# ❌ Zu tief:
/api/studios/1/movies/1/castings/1/awards/
# → Lieber flach mit Filtern!
# ✅ Django-Standard (mit Slash):
router = DefaultRouter()
# ✅ ODER ohne Slash (konsistent):
router = DefaultRouter(trailing_slash=False)
# ❌ Nicht mischen!
# filepath: movies/models.py
from django.db import models
class Movie(models.Model):
title = models.CharField(max_length=200, unique=True)
year = models.IntegerField()
genre = models.CharField(max_length=100)
rating = models.DecimalField(max_digits=3, decimal_places=1, null=True, blank=True)
description = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.title} ({self.year})"
class Artist(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
birth_date = models.DateField(null=True, blank=True)
def __str__(self):
return f"{self.first_name} {self.last_name}"
class MovieCasting(models.Model):
movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='castings')
artist = models.ForeignKey(Artist, on_delete=models.CASCADE, related_name='castings')
role_name = models.CharField(max_length=200)
is_lead = models.BooleanField(default=False)
def __str__(self):
return f"{self.artist} as {self.role_name} in {self.movie}"
# filepath: movies/serializers.py
from rest_framework import serializers
from .models import Movie, Artist, MovieCasting
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
read_only_fields = ['id', 'created_at', 'updated_at']
class ArtistSerializer(serializers.ModelSerializer):
class Meta:
model = Artist
fields = '__all__'
class MovieCastingSerializer(serializers.ModelSerializer):
artist_name = serializers.CharField(source='artist.__str__', read_only=True)
movie_title = serializers.CharField(source='movie.title', read_only=True)
class Meta:
model = MovieCasting
fields = '__all__'
read_only_fields = ['id']
# filepath: movies/views.py
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django.db.models import Avg, Count, Q
from datetime import datetime
from .models import Movie, Artist, MovieCasting
from .serializers import MovieSerializer, ArtistSerializer, MovieCastingSerializer
class MovieViewSet(viewsets.ModelViewSet):
"""
ViewSet für Movie CRUD + Custom Actions
"""
queryset = Movie.objects.all()
serializer_class = MovieSerializer
@action(detail=False, methods=['get'])
def top_rated(self, request):
"""
Top 10 Filme nach Rating
URL: GET /api/movies/top_rated/
"""
top_movies = Movie.objects.filter(
rating__isnull=False
).order_by('-rating')[:10]
serializer = self.get_serializer(top_movies, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def recent(self, request):
"""
Filme der letzten 5 Jahre
URL: GET /api/movies/recent/
"""
current_year = datetime.now().year
recent_movies = Movie.objects.filter(
year__gte=current_year - 5
).order_by('-year')
serializer = self.get_serializer(recent_movies, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def statistics(self, request):
"""
Statistiken über alle Filme
URL: GET /api/movies/statistics/
"""
stats = Movie.objects.aggregate(
total=Count('id'),
avg_rating=Avg('rating'),
oldest_year=models.Min('year'),
newest_year=models.Max('year')
)
stats['by_genre'] = list(
Movie.objects.values('genre')
.annotate(count=Count('id'))
.order_by('-count')
)
return Response(stats)
@action(detail=False, methods=['get'])
def search(self, request):
"""
Filme suchen
URL: GET /api/movies/search/?q=matrix&year=1999&genre=Sci-Fi
"""
queryset = Movie.objects.all()
# Query Parameter auslesen
query = request.query_params.get('q', None)
year = request.query_params.get('year', None)
genre = request.query_params.get('genre', None)
# Filtern
if query:
queryset = queryset.filter(
Q(title__icontains=query) | Q(description__icontains=query)
)
if year:
queryset = queryset.filter(year=year)
if genre:
queryset = queryset.filter(genre__iexact=genre)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def rate(self, request, pk=None):
"""
Film bewerten
URL: POST /api/movies/{pk}/rate/
Body: {"rating": 8.5}
"""
movie = self.get_object()
rating = request.data.get('rating')
# Validierung
try:
rating = float(rating)
if not (0 <= rating <= 10):
return Response(
{'error': 'Rating must be between 0 and 10'},
status=status.HTTP_400_BAD_REQUEST
)
except (TypeError, ValueError):
return Response(
{'error': 'Invalid rating value'},
status=status.HTTP_400_BAD_REQUEST
)
# Speichern
movie.rating = rating
movie.save()
serializer = self.get_serializer(movie)
return Response(serializer.data)
@action(detail=True, methods=['get'])
def cast(self, request, pk=None):
"""
Besetzung eines Films
URL: GET /api/movies/{pk}/cast/
"""
movie = self.get_object()
castings = movie.castings.select_related('artist').all()
serializer = MovieCastingSerializer(castings, many=True)
return Response(serializer.data)
class ArtistViewSet(viewsets.ModelViewSet):
"""
ViewSet für Artist CRUD + Custom Actions
"""
queryset = Artist.objects.all()
serializer_class = ArtistSerializer
@action(detail=True, methods=['get'])
def movies(self, request, pk=None):
"""
Alle Filme eines Artists
URL: GET /api/artists/{pk}/movies/
"""
artist = self.get_object()
castings = artist.castings.select_related('movie').all()
# Unique Movies extrahieren
movies = list(set([casting.movie for casting in castings]))
serializer = MovieSerializer(movies, many=True)
return Response(serializer.data)
class MovieCastingViewSet(viewsets.ModelViewSet):
"""
ViewSet für MovieCasting CRUD
"""
queryset = MovieCasting.objects.select_related('movie', 'artist').all()
serializer_class = MovieCastingSerializer
@action(detail=False, methods=['get'])
def lead_roles(self, request):
"""
Alle Hauptrollen
URL: GET /api/castings/lead_roles/
"""
lead_castings = MovieCasting.objects.filter(
is_lead=True
).select_related('movie', 'artist')
serializer = self.get_serializer(lead_castings, many=True)
return Response(serializer.data)
# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter
from .views import MovieViewSet, ArtistViewSet, MovieCastingViewSet
# Router erstellen
router = DefaultRouter()
# ViewSets registrieren
router.register(r'movies', MovieViewSet, basename='movie')
router.register(r'artists', ArtistViewSet, basename='artist')
router.register(r'castings', MovieCastingViewSet, basename='casting')
# URLs exportieren
urlpatterns = router.urls
# filepath: movieapi/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
# Django Admin
path('admin/', admin.site.urls),
# API URLs
path('api/', include('movies.urls')),
# DRF Auth (Login/Logout für Browsable API)
path('api-auth/', include('rest_framework.urls')),
]
# filepath: movieapi/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# DRF
'rest_framework',
# Apps
'movies',
]
REST_FRAMEWORK = {
# Pagination
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
# Filtering
'DEFAULT_FILTER_BACKENDS': [
'rest_framework.filters.OrderingFilter',
'rest_framework.filters.SearchFilter',
],
# Permissions (optional)
# 'DEFAULT_PERMISSION_CLASSES': [
# 'rest_framework.permissions.IsAuthenticatedOrReadOnly',
# ],
}
# Root API
GET /api/ → API Root (Übersicht)
# Movies - CRUD
GET /api/movies/ → Liste aller Filme (paginiert)
POST /api/movies/ → Film erstellen
GET /api/movies/{id}/ → Film-Details
PUT /api/movies/{id}/ → Film komplett aktualisieren
PATCH /api/movies/{id}/ → Film teilweise aktualisieren
DELETE /api/movies/{id}/ → Film löschen
# Movies - Custom Actions (Collection)
GET /api/movies/top_rated/ → Top 10 Filme
GET /api/movies/recent/ → Filme der letzten 5 Jahre
GET /api/movies/statistics/ → Statistiken
GET /api/movies/search/?q=matrix → Filme suchen
# Movies - Custom Actions (Member)
POST /api/movies/{id}/rate/ → Film bewerten
GET /api/movies/{id}/cast/ → Besetzung anzeigen
# Artists - CRUD
GET /api/artists/ → Liste aller Artists
POST /api/artists/ → Artist erstellen
GET /api/artists/{id}/ → Artist-Details
PUT /api/artists/{id}/ → Artist aktualisieren
PATCH /api/artists/{id}/ → Artist teilweise aktualisieren
DELETE /api/artists/{id}/ → Artist löschen
# Artists - Custom Actions
GET /api/artists/{id}/movies/ → Filme eines Artists
# Castings - CRUD
GET /api/castings/ → Liste aller Castings
POST /api/castings/ → Casting erstellen
GET /api/castings/{id}/ → Casting-Details
PUT /api/castings/{id}/ → Casting aktualisieren
PATCH /api/castings/{id}/ → Casting teilweise aktualisieren
DELETE /api/castings/{id}/ → Casting löschen
# Castings - Custom Actions
GET /api/castings/lead_roles/ → Alle Hauptrollen
# Authentication (Browsable API)
GET /api-auth/login/ → Login
GET /api-auth/logout/ → Logout
python manage.py shell
>>> from django.urls import get_resolver
>>> resolver = get_resolver()
>>> for pattern in resolver.url_patterns:
... print(pattern)
# Oder spezifischer:
>>> from rest_framework.routers import DefaultRouter
>>> from movies.urls import router
>>> for url in router.urls:
... print(url.pattern)
# Django 3.0+:
python manage.py show_urls
# Output:
/api/ → api-root
/api/movies/ → movie-list
/api/movies/{pk}/ → movie-detail
/api/movies/top_rated/ → movie-top-rated
...
# Browser öffnen:
http://localhost:8000/api/
# DefaultRouter zeigt automatisch alle Endpoints!
# filepath: movies/management/commands/list_urls.py
from django.core.management.base import BaseCommand
from django.urls import get_resolver
class Command(BaseCommand):
help = 'Liste alle URLs'
def handle(self, *args, **options):
resolver = get_resolver()
def show_urls(urllist, depth=0):
for entry in urllist:
print(" " * depth + str(entry.pattern))
if hasattr(entry, 'url_patterns'):
show_urls(entry.url_patterns, depth + 1)
show_urls(resolver.url_patterns)
# Verwendung:
python manage.py list_urls
# Installation:
pip install django-extensions
# settings.py:
INSTALLED_APPS = [
# ...
'django_extensions',
]
# Alle URLs anzeigen:
python manage.py show_urls
# Mit Details:
python manage.py show_urls --format table
from django.urls import re_path
urlpatterns = [
re_path(
r'^api/movies/(?P[0-9]{4})/(?P[0-9]{2})/(?P[0-9]{2})/$',
views.movies_by_date
),
]
# ← Langsam! RegEx muss bei jedem Request gematched werden
from django.urls import path
urlpatterns = [
path('api/movies/', views.movie_list),
path('api/movies//', views.movie_detail),
]
# ← Schnell! Django cached URL-Patterns
urlpatterns = [
path('movies/', MovieListView.as_view()),
path('movies//', MovieDetailView.as_view()),
path('movies/top/', TopMoviesView.as_view()),
path('movies/recent/', RecentMoviesView.as_view()),
path('movies/search/', SearchView.as_view()),
path('movies//rate/', RateView.as_view()),
path('movies//cast/', CastView.as_view()),
# ... 20 mehr URLs
]
# ← Viele Views, viele URLs = mehr Code
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
@action(detail=False)
def top(self, request): pass
@action(detail=False)
def recent(self, request): pass
@action(detail=False)
def search(self, request): pass
@action(detail=True)
def rate(self, request, pk): pass
@action(detail=True)
def cast(self, request, pk): pass
router = DefaultRouter()
router.register(r'movies', MovieViewSet)
# ← 1 ViewSet, automatische URLs!
path() bevorzugen über re_path()include() für App-URLs (bessere Organisation)# FEHLER:
class MovieViewSet(viewsets.ModelViewSet):
serializer_class = MovieSerializer
def get_queryset(self):
return Movie.objects.all()
# Kein queryset-Attribut!
router.register(r'movies', MovieViewSet)
# ← Error: basename required!
# FIX:
router.register(r'movies', MovieViewSet, basename='movie')
# FEHLER:
urlpatterns = [
path('movies//', movie_detail), # ← Matched zuerst!
path('movies/top/', top_movies), # ← Wird nie erreicht!
]
# /movies/top/ wird als /movies// interpretiert!
# FIX:
urlpatterns = [
path('movies/top/', top_movies), # ← Spezifisch zuerst!
path('movies//', movie_detail), # ← Generisch zuletzt
]
# FEHLER:
urlpatterns = [
path('movies/', movie_list), # Mit Slash
path('artists', artist_list), # Ohne Slash
]
# FIX:
urlpatterns = [
path('movies/', movie_list),
path('artists/', artist_list), # Konsistent!
]
# FEHLER:
urlpatterns = router.urls + [
path('custom/', custom_view),
]
# Custom URLs werden NACH Router-URLs gematcht!
# FIX:
urlpatterns = [
path('custom/', custom_view), # Spezifisch zuerst
] + router.urls
# FEHLER (movieapi/urls.py):
from movies.views import MovieViewSet
urlpatterns = [
path('api/movies/', MovieViewSet.as_view()), # ← Falsch!
]
# FIX:
from django.urls import include
urlpatterns = [
path('api/', include('movies.urls')), # ← Korrekt!
]
# FEHLER:
urlpatterns = [
path('movies/', MovieListView), # ← Klasse statt Instance!
]
# FIX:
urlpatterns = [
path('movies/', MovieListView.as_view()), # ← Korrekt!
]
~15 URL-Patterns
urlpatterns = [
# CRUD
path('movies/', MovieListAPIView.as_view()),
path('movies//', MovieDetailAPIView.as_view()),
# Custom
path('movies/top/', TopMoviesView.as_view()),
path('movies/recent/', RecentMoviesView.as_view()),
path('movies/search/', SearchView.as_view()),
path('movies//rate/', RateView.as_view()),
path('movies//cast/', CastView.as_view()),
# Artists
path('artists/', ArtistListAPIView.as_view()),
path('artists//', ArtistDetailAPIView.as_view()),
path('artists//movies/', ArtistMoviesView.as_view()),
# Castings
path('castings/', CastingListAPIView.as_view()),
path('castings//', CastingDetailAPIView.as_view()),
path('castings/lead/', LeadRolesView.as_view()),
]
✅ Vorteile: Maximale Kontrolle
❌ Nachteile: Viel Code, fehleranfällig
~10 URL-Patterns
urlpatterns = [
# CRUD (weniger Views)
path('movies/', MovieListCreateView.as_view()),
path('movies//', MovieDetailView.as_view()),
# Custom (noch manuell)
path('movies/top/', TopMoviesView.as_view()),
path('movies/recent/', RecentMoviesView.as_view()),
path('movies/search/', SearchView.as_view()),
# Artists
path('artists/', ArtistListCreateView.as_view()),
path('artists//', ArtistDetailView.as_view()),
# Castings
path('castings/', CastingListCreateView.as_view()),
path('castings//', CastingDetailView.as_view()),
]
✅ Vorteile: Weniger Code als APIView
❌ Nachteile: URLs noch manuell
~5 Zeilen Code!
# ALLE URLs automatisch!
router = DefaultRouter()
router.register(r'movies', MovieViewSet)
router.register(r'artists', ArtistViewSet)
router.register(r'castings', MovieCastingViewSet)
urlpatterns = router.urls
# Custom Actions im ViewSet:
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
@action(detail=False)
def top(self, request): pass
@action(detail=False)
def recent(self, request): pass
@action(detail=False)
def search(self, request): pass
@action(detail=True)
def rate(self, request, pk): pass
@action(detail=True)
def cast(self, request, pk): pass
✅ Vorteile: Minimal Code, automatisch, RESTful
⭐ Production-Ready!
# DefaultRouter (Empfohlen!)
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'movies', MovieViewSet)
urlpatterns = router.urls
# Mit basename (bei get_queryset())
router.register(r'movies', MovieViewSet, basename='movie')
# Ohne Trailing Slash
router = DefaultRouter(trailing_slash=False)
# Collection Action
@action(detail=False, methods=['get'])
def top_rated(self, request):
pass
# URL: /movies/top_rated/
# Member Action
@action(detail=True, methods=['post'])
def rate(self, request, pk):
pass
# URL: /movies/{pk}/rate/
# Custom URL-Path
@action(detail=False, url_path='top-10')
def top_rated(self, request):
pass
# URL: /movies/top-10/
# reverse() in Code
from django.urls import reverse
url = reverse('movie-list')
url = reverse('movie-detail', kwargs={'pk': 1})
url = reverse('movie-top-rated')
# In Templates
{% url 'movie-list' %}
{% url 'movie-detail' pk=movie.id %}
# Installation
pip install drf-nested-routers
# Setup
from rest_framework_nested import routers
router = routers.DefaultRouter()
router.register(r'movies', MovieViewSet)
movies_router = routers.NestedDefaultRouter(
router, r'movies', lookup='movie'
)
movies_router.register(
r'castings', MovieCastingViewSet
)
urlpatterns = router.urls + movies_router.urls
# settings.py
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS':
'rest_framework.versioning.URLPathVersioning',
}
# urls.py
urlpatterns = [
path('api/v1/', include(router_v1.urls)),
path('api/v2/', include(router_v2.urls)),
]
# path() Converter
# Integer
# String
# UUID
# Pfad (mehrere Segmente)
# Beispiele
path('movies//', ...)
path('movies//', ...)
path('files//', ...)
JWT Tokens
Session Auth
Custom Permissions
django-filter
Custom Filters
Pagination Classes
APITestCase
URL Tests
Integration Tests
drf-spectacular
Swagger/OpenAPI
API Docs
Production Settings
Docker
CORS & Security
pip install drf-nested-routers
pip install django-extensions
python manage.py show_urlsImplementiere eigene API mit automatischem Routing!
Keep coding, keep learning! 💻